package cn.com.zero.common.auth.security.service;

import cn.com.zero.api.auth.constant.HttpHeaderConstants;
import cn.com.zero.api.auth.util.TokenUtil;
import cn.com.zero.api.auth.util.UserCacheKeyUtil;
import cn.com.zero.api.auth.vo.LockedUserVO;
import cn.com.zero.api.auth.vo.OnlineUserVO;
import cn.com.zero.base.util.RedisUtil;
import cn.com.zero.common.auth.config.properties.AuthProperties;
import cn.com.zero.common.auth.enums.LoginLogResultEnum;
import cn.com.zero.common.auth.enums.LoginLogTypeEnum;
import cn.com.zero.common.auth.security.po.AuthUserPO;
import cn.com.zero.common.auth.security.po.ErrorLoginLogPO;
import cn.com.zero.common.auth.security.po.LoginLogPO;
import cn.com.zero.api.auth.po.MenuPO;
import cn.com.zero.common.auth.security.vo.LoginUserInfoVO;
import cn.com.zero.web.util.HttpRequestUtil;
import cn.hutool.core.util.StrUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * @author xiongxj
 * @version 1.0.0
 * @Description 登录认证service抽象类（注意：实现类必须注入spring容器管理）
 * @createTime 2023/3/23 9:35
 */
public abstract class AbstractLoginAuthenticationService {
    protected Logger logger = LoggerFactory.getLogger(this.getClass());
    private TokenUtil tokenUtil;

    private RedisUtil redisUtil;

    private AuthProperties authProperties;

    /**
     * 获取当前认证用户所属端信息，用于操作Redis缓存信息。例如：，比如Manage、front等
     *
     * @return 当前认证用户所属端信息
     */
    public abstract String getAuthenticationSide();

    /**
     * 根据手机号查询用户（登录时用户输入的唯一手机号）
     *
     * @param mobile 手机号码
     * @return 用户信息
     */
    public abstract AuthUserPO getUserByMobile(String mobile);

    /**
     * 根据用户名或者手机号查询用户
     *
     * @param userPrinciple 用户名或者手机号
     * @return 用户信息
     */
    public abstract AuthUserPO getUserByNameOrMobile(String userPrinciple);

    /**
     * 根据用户ID查询用户的菜单及功能权限信息
     *
     * @param userId 用户ID
     * @return 菜单及功能列表权限
     */
    public abstract List<MenuPO> getMenusByUserId(String userId);

    /**
     * 校验手机号登录验证码
     *
     * @param mobile           手机号
     * @param verificationCode 验证码
     * @return 校验结果，0表示通过，其他返回码则表示失败
     */
    public abstract Long checkLoginVerificationCode(String mobile, String verificationCode);

    /**
     * 用户登录成功后的监听器处理逻辑
     * 登录成功后，重置失败次数为 0，添加登录成功日志
     *
     * @param authUser 登录成功时的用户信息
     */
    @Transactional(rollbackFor = Throwable.class)
    public void loginSuccessListenerProcess(AuthUserPO authUser) {
        String ipAddress = HttpRequestUtil.getRequestIp();
        String loginDevice = HttpRequestUtil.getHeader(HttpHeaderConstants.LOGIN_DEVICE_HEADER);
        String userId = authUser.getUserId();

        // 更改用户的信息
        AuthUserPO authUserPO = new AuthUserPO();
        authUserPO.setUserId(userId);
        authUserPO.setLoginIp(ipAddress);
        authUserPO.setLoginTime(authUser.getLoginTime());
        authUserPO.setLoginDevice(loginDevice);
        this.modifyUserInfo(authUserPO);

        // 删除错误次数
        this.deleteErrorLoginLog(userId);

        // 添加登录成功日志
        LoginLogPO loginLogPO = new LoginLogPO(userId, loginDevice, ipAddress, authUser.getLoginTime());
        loginLogPO.setType(LoginLogTypeEnum.LOGIN.getValue());
        loginLogPO.setResult(LoginLogResultEnum.SUCCESS.getValue());
        loginLogPO.setRemark("登录成功");
        this.addUserLoginLog(loginLogPO);
        logger.info("[{}]端用户[{{}]在[{}]设备登录成功", this.getAuthenticationSide(), userId, loginDevice);
    }

    /**
     * 用户登录失败后的监听器处理逻辑（仅针对用户名不存在、密码错误、手机号不存在的情况）
     *
     * @param userPrinciple 用户名或者手机号
     */
    @Transactional(rollbackFor = Throwable.class)
    public void loginFailureListenerProcess(String userPrinciple) {
        AuthUserPO authUser = this.getUserByNameOrMobile(userPrinciple);
        if (null == authUser) {
            return;
        }

        // 获取当前失败次数
        String userId = authUser.getUserId();
        int errorLoginTimes = this.getErrorLoginTimes(userId);
        String ipAddress = HttpRequestUtil.getRequestIp();
        String loginDevice = HttpRequestUtil.getHeader(HttpHeaderConstants.LOGIN_DEVICE_HEADER);

        // 添加登录失败日志
        LoginLogPO loginLogPO = new LoginLogPO(userId, loginDevice, ipAddress, authUser.getLoginTime());
        loginLogPO.setType(LoginLogTypeEnum.LOGIN.getValue());
        loginLogPO.setResult(LoginLogResultEnum.FAILURE.getValue());
        loginLogPO.setRemark("登录失败，用户名或者密码错误");
        this.addUserLoginLog(loginLogPO);

        // 添加错误登录日志
        ErrorLoginLogPO errorLoginLogPO = new ErrorLoginLogPO(userId, loginDevice, ipAddress);
        this.addUserErrorLoginLog(errorLoginLogPO);

        // 因为本次又插入一条错误记录，所以错误次数加1
        errorLoginTimes++;
        // 如果达到设定的密码错误次数阈值则锁定用户
        if (errorLoginTimes >= authProperties.getPermitFailedTimes()) {
            // 用户被锁定
            String lockedUserCacheKey = UserCacheKeyUtil.getLockedUserCacheKey(this.getAuthenticationSide(), userId);
            // 组装被锁定的用户对象
            LockedUserVO lockedUser = new LockedUserVO();
            lockedUser.setUserId(userId);
            lockedUser.setLoginIp(ipAddress);
            lockedUser.setLoginTime(authUser.getLoginTime());
            Date lockedTime = new Date();
            lockedUser.setLockedTime(lockedTime);
            redisUtil.setObjectAndTime(lockedUserCacheKey, lockedUser, authProperties.getAutoUnlockUserTime(), TimeUnit.MINUTES);
            logger.info("[{}]端用户[{{}]在[{}]设备登录失败，达到错误密码错误次数阈值[{}]，此用户被锁定", this.getAuthenticationSide(),
                    userId, loginDevice, authProperties.getPermitFailedTimes());

            // 锁定用户时，需要将用户所有的登录端都踢下线
            String onlineUserKey = UserCacheKeyUtil.getOnlineUserCacheKey(this.getAuthenticationSide(),
                    null, authUser.getUserId());
            Set<String> onlineUserKeys = redisUtil.keys(onlineUserKey);
            String tokenIssueTimeKey = UserCacheKeyUtil.getUserTokenIssueTimeCacheKey(this.getAuthenticationSide(),
                    null, authUser.getUserId());
            Set<String> tokenIssueTimeKeys = redisUtil.keys(tokenIssueTimeKey);
            String userPermissionKey = UserCacheKeyUtil.getMenuPermissionCacheKey(this.getAuthenticationSide(),
                    null, authUser.getUserId());
            Set<String> userPermissionKeys = redisUtil.keys(userPermissionKey);
            Set<String> totalKeys = new HashSet<>();
            if (!CollectionUtils.isEmpty(onlineUserKeys)) {
                totalKeys.addAll(onlineUserKeys);
            }
            if (!CollectionUtils.isEmpty(tokenIssueTimeKeys)) {
                totalKeys.addAll(tokenIssueTimeKeys);
            }
            if (!CollectionUtils.isEmpty(userPermissionKeys)) {
                totalKeys.addAll(userPermissionKeys);
            }
            if (!CollectionUtils.isEmpty(totalKeys)) {
                redisUtil.delete(totalKeys);
                logger.info("[{}]端用户[{{}]在[{}]设备登录失败后被锁定，强制下线此用户所有在线设备信息",
                        this.getAuthenticationSide(), userId, loginDevice);
            }
        }
    }

    /**
     * 保存用户的登录信息
     *
     * @param authUser    认证后的用户信息
     * @param loginDevice 当前认证用户的登录设备
     * @return 用户完整的登录信息（包括权限等）
     */
    public LoginUserInfoVO saveLoginUser(AuthUserPO authUser, String loginDevice) {
        String ipAddress = HttpRequestUtil.getRequestIp();
        // 获取当前认证用户所属端信息
        String userBelongToSide = this.getAuthenticationSide();

        // 当前在线用户、当前用户token签发时间、当前用户菜单权限缓存key
        String onlineUserKey = UserCacheKeyUtil.getOnlineUserCacheKey(userBelongToSide,
                loginDevice, authUser.getUserId());
        String tokenIssueTimeKey = UserCacheKeyUtil.getUserTokenIssueTimeCacheKey(userBelongToSide,
                loginDevice, authUser.getUserId());
        String userPermissionKey = UserCacheKeyUtil.getMenuPermissionCacheKey(userBelongToSide,
                loginDevice, authUser.getUserId());

        // 清除当前用户的所有缓存信息
        redisUtil.delete(onlineUserKey, tokenIssueTimeKey, userPermissionKey);

        // 获取用户特定的菜单权限
        List<MenuPO> userMenuList = this.getMenusByUserId(authUser.getUserId());
        // 生成token
        String token = tokenUtil.generateToken(authUser.getUserId(), authUser.getLoginTime());

        // 保存用户的在线信息、token签发时间和菜单权限
        OnlineUserVO onlineUserVO = new OnlineUserVO(authUser.getUserId(), loginDevice, authUser.getLoginTime(), ipAddress);
        redisUtil.setObjectAndTime(onlineUserKey, onlineUserVO, tokenUtil.getTokenProperties().getExpireTime(), TimeUnit.SECONDS);
        redisUtil.setObjectAndTime(tokenIssueTimeKey, authUser.getLoginTime(), tokenUtil.getTokenProperties().getExpireTime(), TimeUnit.SECONDS);
        if (!CollectionUtils.isEmpty(userMenuList)) {
            redisUtil.setListAndTime(userPermissionKey, userMenuList, tokenUtil.getTokenProperties().getExpireTime(), TimeUnit.SECONDS);
        }

        // 组装 给前端返回登录成功的用户信息
        LoginUserInfoVO loginUserInfo = new LoginUserInfoVO();
        loginUserInfo.cloneUserInfo(authUser);
        loginUserInfo.setToken(token);
        // 设置用户的菜单权限
        loginUserInfo.setMenuList(userMenuList);
        // 设置用户无需登录就可访问的权限
        List publicPermissionList = redisUtil.getList(UserCacheKeyUtil.getPublicPermissionCacheKey(userBelongToSide));
        loginUserInfo.setPublicPermissionList(publicPermissionList);
        // 设置用户只要登录就可访问的权限
        List publicLoginPermissionList = redisUtil.getList(UserCacheKeyUtil.getPublicLoginPermissionCacheKey(userBelongToSide));
        loginUserInfo.setPublicPermissionList(publicLoginPermissionList);

        logger.info("[{}]端用户[{{}]在[{}]设备登录成功后，成功缓存用户信息", this.getAuthenticationSide(), authUser.getUserId(), loginDevice);
        return loginUserInfo;
    }

    /**
     * 退出处理
     *
     * @param token       token令牌
     * @param loginDevice 登录设备
     */
    public void logoutHandler(String token, String loginDevice) {
        if (StrUtil.isEmpty(token)) {
            return;
        }
        if (StrUtil.isEmpty(loginDevice)) {
            return;
        }
        String userId = tokenUtil.getTokenSubject(token, String.class);
        if (StrUtil.isEmpty(userId)) {
            return;
        }

        // 获取当前认证用户所属端信息
        String userBelongToSide = this.getAuthenticationSide();

        // 当前在线用户、当前用户token签发时间、当前用户菜单权限缓存key
        String onlineUserKey = UserCacheKeyUtil.getOnlineUserCacheKey(userBelongToSide,
                loginDevice, userId);
        String tokenIssueTimeKey = UserCacheKeyUtil.getUserTokenIssueTimeCacheKey(userBelongToSide,
                loginDevice, userId);
        String userPermissionKey = UserCacheKeyUtil.getMenuPermissionCacheKey(userBelongToSide,
                loginDevice, userId);

        // 清除当前用户的所有缓存信息
        Long deleteCount = redisUtil.delete(onlineUserKey, tokenIssueTimeKey, userPermissionKey);
        if (deleteCount.compareTo(0L) == 0) {
            // 表明用户已经退出了
            return;
        }

        // 添加用户主动退出日志
        String ipAddress = HttpRequestUtil.getRequestIp();
        LoginLogPO loginLogPO = new LoginLogPO();
        loginLogPO.setUserId(userId);
        loginLogPO.setLoginDevice(loginDevice);
        loginLogPO.setLoginIp(ipAddress);
        loginLogPO.setType(LoginLogTypeEnum.LOGOUT.getValue());
        loginLogPO.setResult(LoginLogResultEnum.SUCCESS.getValue());
        loginLogPO.setRemark("用户主动退出登录");
        this.addUserLoginLog(loginLogPO);
        logger.info("[{}]端用户[{{}]在[{}]设备主动退出成功", this.getAuthenticationSide(), userId, loginDevice);
    }

    /**
     * 更改用户信息
     *
     * @param userPO 待更改的用户信息内容
     */
    protected abstract void modifyUserInfo(AuthUserPO userPO);

    /**
     * 删除用户的错误登录日志（主要用于统计次数，便于锁定的那个日志）
     *
     * @param userId 用户ID
     */
    protected abstract void deleteErrorLoginLog(String userId);

    /**
     * 添加登录日志
     *
     * @param loginLogPO 登录日志信息
     */
    protected abstract void addUserLoginLog(LoginLogPO loginLogPO);

    /**
     * 获取用户登录错误的次数
     *
     * @param userId 用户ID
     * @return 登录错误次数
     */
    protected abstract int getErrorLoginTimes(String userId);

    /**
     * 添加用户错误登录日志
     *
     * @param errorLoginLogPO 错误登录日志信息
     */
    protected abstract void addUserErrorLoginLog(ErrorLoginLogPO errorLoginLogPO);

    @Resource
    public void setTokenUtil(TokenUtil tokenUtil) {
        this.tokenUtil = tokenUtil;
    }

    @Resource
    public void setRedisUtil(RedisUtil redisUtil) {
        this.redisUtil = redisUtil;
    }

    @Autowired
    public void setAuthProperties(AuthProperties authProperties) {
        this.authProperties = authProperties;
    }
}
